// 
//  Author:
//    anton 
// 
//  Copyright (c) 2010, anton
// 
//  All rights reserved.
// 
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
// 
//     * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in
//       the documentation and/or other materials provided with the distribution.
//     * Neither the name of the [ORGANIZATION] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
// 
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
//  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
//  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
//  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
//  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
//  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// 

using System;
using System.IO;
using System.Collections.Generic;

using OpenMetaverse;

namespace OpenSimCache
{
	public class TextureCache
	{
		private string mRootPath;

		Dictionary<UUID, Entry> mSubEntries = null;
		List<Entry> mEntries;
		
		struct EntriesHeader
		{
			public Single  Version;
			public UInt32  EntryCount;
			
			public EntriesHeader(BinaryReader entriesReader)
			{
				Version = entriesReader.ReadSingle();
				EntryCount = entriesReader.ReadUInt32();
			}
		}

		struct Entry
		{
			public UUID  ID;
			public Int32 ImageSize;
			public Int32 BodySize;
			public DateTime Time;
			
			public Entry(BinaryReader entriesReader, Dictionary<UUID, Entry> subEntries)
			{
				ID = new UUID(entriesReader.ReadBytes(16), 0);
				ImageSize = entriesReader.ReadInt32();
				
				if(subEntries != null)
					if(subEntries.ContainsKey(ID))
						BodySize = subEntries[ID].BodySize;
					else
						BodySize = -999;
				else
					BodySize = entriesReader.ReadInt32();
				
				Time = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(entriesReader.ReadUInt32());
			}

			public Entry(BinaryReader entriesReader)
			{
				ID = new UUID(entriesReader.ReadBytes(16), 0);
				BodySize = entriesReader.ReadInt32();
				ImageSize = 0;
				Time = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(entriesReader.ReadUInt32());
			}

		}

		public TextureCache(string rootPath)
		{
			mRootPath = rootPath;

			// load the various entries
			using(FileStream entriesStream = new FileStream(Path.Combine(rootPath, "texture.entries"), FileMode.Open, FileAccess.Read))
			{
				using(BinaryReader entriesReader = new BinaryReader(entriesStream))
				{
					EntriesHeader header = new EntriesHeader(entriesReader);

					// we need the size of the body, which in version 1.0 and below is found in "textures/texture.entries"
					if(header.Version <= 1.0f)
						LoadSubEntries();
					
					// now load the individual entries
					mEntries = new List<TextureCache.Entry>();
					while(entriesStream.Position < entriesStream.Length-1)
						mEntries.Add(new Entry(entriesReader, mSubEntries));
				}
			}
		}
		
		private void LoadSubEntries()
		{
			mSubEntries = new Dictionary<UUID, TextureCache.Entry>();			

			// load the entries from the "textures/texture.entries" file
			using(FileStream subEntriesStream = new FileStream(Path.Combine(mRootPath, "textures/texture.entries"), FileMode.Open, FileAccess.Read))
				using(BinaryReader subEntriesReader = new BinaryReader(subEntriesStream))
				{
					while(subEntriesStream.Position < subEntriesStream.Length)
					{	
						Entry entry = new Entry(subEntriesReader);
						
						if(mSubEntries.ContainsKey(entry.ID))
						{
							if(mSubEntries[entry.ID].Time < entry.Time)
								mSubEntries[entry.ID] = entry;
						}
						else
							mSubEntries.Add(entry.ID, entry);	
					}
				}
		}
		
		public void Dump()
		{
			foreach(Entry e in mEntries)
				Console.WriteLine(e.ID + ";" + e.ImageSize + ";" + e.BodySize + ";" + e.Time);
		}
		
		private byte[] GetHeader(int offset, int size)
		{
			// grab the first 600 bytes from "texture.cache"
			int headerCacheSize = Math.Min(600, size);
			
			byte[] data = new byte[headerCacheSize];
			using(FileStream smallCache = new FileStream(Path.Combine(mRootPath, "texture.cache"), FileMode.Open, FileAccess.Read))
			{
				smallCache.Seek(offset*600, SeekOrigin.Begin);
				smallCache.Read(data, 0, headerCacheSize);
			}
			
			return data;
		}
		
		private byte[] GetBody(string fileName, int size)
		{
			byte[] data = new byte[size];
			using(FileStream smallCache = new FileStream(fileName, FileMode.Open, FileAccess.Read))
			{
				smallCache.Read(data, 0, size);
			}
			
			return data;
		}

		private string GetBodyFileName(UUID id)
		{
			string idAsString = id.ToString();
			return Path.Combine(mRootPath, Path.Combine("textures", Path.Combine(idAsString.Substring(0, 1), idAsString)));
		}
		
		public byte[] GetImage(UUID id)
		{
			int idx = 0;
			while(idx < mEntries.Count)
			{
				if(mEntries[idx].ID == id)
					break;
				idx++;
			}

			// sometimes the body is missing, if so, we return null
			if(mEntries[idx].ImageSize > 600 && !File.Exists(GetBodyFileName(id)))
			   return null;
			
			int imageSize = mEntries[idx].ImageSize;
			int bodySize = mEntries[idx].BodySize;
			
			// sometimes imagesize is -1 and bodysize contains the actual body size;
			if(imageSize == -1)
			{
				imageSize = bodySize+600;
			}
			
			byte[] data = new byte[imageSize];
			GetHeader(idx, imageSize).CopyTo(data, 0);
			
			if(imageSize > 600)
			{
				string fileName = GetBodyFileName(id);
				GetBody(fileName, imageSize - 600).CopyTo(data, 600);
			}
			return data;
		}
		
		public IEnumerable<UUID> ImageIDs()
		{
			int idx = -1;
			while(idx < mEntries.Count-1)
			{
				idx++;

				// check for invalid combinations
				if(mEntries[idx].ImageSize > 600 && !File.Exists(GetBodyFileName(mEntries[idx].ID)))
					continue;

				if(mEntries[idx].ImageSize == -1 && !File.Exists(GetBodyFileName(mEntries[idx].ID)))
					continue;

				yield return mEntries[idx].ID;
			}
		}
	}
}
